package net.pulseproject.commons.midi.mediator;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Receiver;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Transmitter;

import net.pulseproject.commons.midi.MessageAnalyzer;
import net.pulseproject.commons.midi.MidiDeviceCache;
import net.pulseproject.commons.midi.Receivable;
import net.pulseproject.commons.midi.Transmitable;
import net.pulseproject.commons.midi.collection.MidiDeviceFilter;
import net.pulseproject.commons.midi.collection.ReceiverCollection;
import net.pulseproject.commons.midi.entity.DeviceKind;
import net.pulseproject.commons.midi.entity.MidiSocket;
import net.pulseproject.commons.misc.collection.EnablerCollection;
import net.pulseproject.commons.misc.collection.EnablerCollectionImpl;
import net.pulseproject.commons.util.MidiUtil;
import net.pulseproject.router.modules.misc.ModuleFilter;
import net.pulseproject.router.modules.misc.RoutingTable;
import net.pulseproject.router.modules.useable.MidiReceiverModule;
import net.pulseproject.router.modules.useable.MidiTransmitterModule;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

class MediatorImpl implements Mediator, MidiReceiverListener {
	
	private static final Log LOG = LogFactory.getLog(MediatorImpl.class);
	
	
	private final MidiReceiverBuilder receiverBuilder;
	
	private final MidiDeviceCache deviceCache;
	
	private final RoutingTable table;
	
	private final ReceiverCollection receiverCollection;
	
	private final Collection<Transmitter> openedMidiTransmitters = new HashSet<Transmitter>();
	
	private final Collection<Receiver> openedMidiReceivers = new HashSet<Receiver>();

	private final Map<String, Receiver> openedMidiReceiversByPort = new HashMap<String, Receiver>();
	
	// TODO MINOR: inject filter dependency (create interfaces for all filters (?))
	private final MidiDeviceFilter midiDeviceFilter = new MidiDeviceFilter();
	private final ModuleFilter moduleFilter = new ModuleFilter();
	
	private boolean yetActivated = false;
	private boolean yetClosed = false;
	
	private final EnablerCollectionWrapper collectionWrapper;
	
	private final Collection<MidiSocket> socketsUsedByPulseOwnTransmitterModules = new HashSet<MidiSocket>();
	
	private final MessageAnalyzer messageAnalyzer;
	
	public MediatorImpl(final MidiReceiverBuilder receiverBuilder,
						final MidiDeviceCache deviceCache,
						final EnablerCollectionWrapper collectionWrapper,
						final MessageAnalyzer messageAnalyzer,
						final RoutingTable table) {
		LOG.debug("new MediatorImpl(receiverBuilder, receiverBuilder, collectionWrapper, messageAnalyzer, table)");
		
		this.receiverBuilder = receiverBuilder;
		this.deviceCache = deviceCache;
		this.collectionWrapper = collectionWrapper;
		this.messageAnalyzer = messageAnalyzer;
		this.table = table;
		this.receiverCollection = new ReceiverCollection(this.table.getTransmitterModules());
	}
	
	public final ActivatedRoutingTable activate() {
		LOG.info("activate()");
		if(this.yetActivated == true) {
			throw new IllegalStateException("Mediator was yet activated!");
		}
		this.yetActivated = true;
		
		final Collection<MidiDevice> allMidiDevices = this.deviceCache.fetchMidiDevices();
		final Collection<MidiTransmitterModule> startpointTransmitters = this.table.getTransmitterModules();
		final Collection<MidiReceiverModule> endpointReceivers =
			this.moduleFilter.filterEndModules(startpointTransmitters);
		
		LOG.debug("  ... allMidiDevices.size=" + allMidiDevices.size());
		LOG.debug("  ... startpointTransmitters=" + Arrays.toString(startpointTransmitters.toArray()) + ")");
		LOG.debug("  ... endpointReceivers=" + Arrays.toString(endpointReceivers.toArray()) + ")");
		
		this.activateTransmitters(allMidiDevices, startpointTransmitters);
		final Collection<MidiSocket> outgoingSockets = this.activateReceivers(allMidiDevices, endpointReceivers);
		this.activateRerouting(endpointReceivers);
		
		LOG.info("activate() COMPLETE ================================");
		return new ActivatedRoutingTable(outgoingSockets);
	}
	
	private void activateTransmitters(final Collection<MidiDevice> allMidiDevices,
			final Collection<MidiTransmitterModule> startpointTransmitters) {
		LOG.debug("activateTransmitters(allMidiDevices, startpointTransmitters)");
		
		final Map<String, Collection<MidiTransmitterModule>> transmittersByPorts =
			this.moduleFilter.modulesByPort(startpointTransmitters);
		
		for (final String transmitterPort : transmittersByPorts.keySet()) {
			final MidiDevice device = this.midiDeviceFilter.searchMidiDevice(allMidiDevices,
					transmitterPort, DeviceKind.TRANSMITTER);
			
			final Transmitter realMidiTransmitter;
			try {
				LOG.debug("Opening MIDI transmitter for port [" + transmitterPort + "] ...");
//				TODO MidiDevice.getTransmitter is not a getter but hides logic! See #getReceiver for notes + solution.
				realMidiTransmitter = device.getTransmitter();
			} catch(final MidiUnavailableException e) {
				throw new RuntimeException("Could not open transmitter port [" + transmitterPort + "] for " +
						"midi device " + device + "!", e);
			}
			this.openedMidiTransmitters.add(realMidiTransmitter);
			
			final MidiReceiverListener listener = this; // see #onMidiReceived
			final Receiver midiReceiver = this.receiverBuilder.build(transmitterPort, listener);
			realMidiTransmitter.setReceiver(midiReceiver);
		}
	}
	
	private Collection<MidiSocket> activateReceivers(final Collection<MidiDevice> allDevices,
			final Collection<MidiReceiverModule> endpointReceivers) {
		LOG.debug("activateReceivers(allMidiDevices, startpointTransmitters)");
		
		final Collection<MidiSocket> outgoingSockets = new HashSet<MidiSocket>();
		for (final MidiReceiverModule midiReceiverModule : endpointReceivers) {
			final MidiSocket socket = midiReceiverModule.getSocket();
			outgoingSockets.add(socket);
			this.socketsUsedByPulseOwnTransmitterModules.add(socket);
		}
		
		final Map<String, Collection<MidiReceiverModule>> receiversByPorts =
			this.moduleFilter.modulesByPort(endpointReceivers);
		
		for (final String port : receiversByPorts.keySet()) {
			final MidiDevice device = this.midiDeviceFilter.searchMidiDevice(allDevices, port, DeviceKind.RECEIVER);
			
			final Receiver midiReceiver = MidiUtil.openReceiver(port, device);
			this.openedMidiReceivers.add(midiReceiver);
			this.openedMidiReceiversByPort.put(port, midiReceiver);
		}
		
		return outgoingSockets;
	}
	

	private void activateRerouting(final Collection<MidiReceiverModule> endpointReceivers) {
		final Map<MidiSocket, ReroutingListener> listenersBySockts = new HashMap<MidiSocket, ReroutingListener>();
		
		for (final MidiReceiverModule currentModule : endpointReceivers) {
			final MidiSocket socket = currentModule.getSocket();
			final String port = socket.getPort();
			final Receiver openedMidiReceiver = this.openedMidiReceiversByPort.get(port);
			if(openedMidiReceiver == null) {
				throw new RuntimeException("Could not get MIDI receiver port by name [" + port + "]!");
			}
			
			final ReroutingListener listener = new ReroutingListener(openedMidiReceiver, socket, this.messageAnalyzer);
			currentModule.addListener(listener);
			listenersBySockts.put(socket, listener);
		}
		
		final EnablerCollection<MidiSocket, ReroutingListener> enablerCollection =
			new EnablerCollectionImpl<MidiSocket, ReroutingListener>(listenersBySockts);
		this.collectionWrapper.setEnablerCollection(enablerCollection);
	}
	
	
	@Override
	public final void close() {
		LOG.info("close()");
		
		if(this.yetClosed == true) {
			throw new IllegalStateException("Mediator was yet closed!");
		}
		this.yetClosed = true;
		
		for (final Transmitter transmitter : this.openedMidiTransmitters) {
			transmitter.close();
		}
		for (final Receiver receiver : this.openedMidiReceivers) {
			receiver.close();
		}
	}

	@Override
	public void onMidiReceived(final ShortMessage message, final Transmitable transmitter, final MidiSocket socket) {
		LOG.trace("onMidiReceived(message=" + MidiUtil.toString(message) + ", transmitter=" + transmitter + ", " +
				"socket=" + socket + ")");
		
		if(this.socketsUsedByPulseOwnTransmitterModules.contains(socket) == true) {
			LOG.debug("Droping MIDI message, as it seems to come from a Pulse Router configured module.");
			return;
		}
		
		final Collection<Receivable> foundReceivers = this.receiverCollection.findReceivables(socket);
		LOG.trace("  ... going to dispatch message to " + foundReceivers.size() + " receiver(s).");
		
		for (final Receivable receivable : foundReceivers) {
			LOG.trace("receivable is: " + receivable);
			receivable.receive(message, transmitter);
		}
	}
	
	@Override
	public String toString() {
		return "MediatorImpl[]";
	}
}
